3-2 密码安全实操:使用argon2库对密码进行加密
本节通过实操演示如何在 NestJS 项目中集成 argon2 库,实现用户注册时的密码哈希加密和登录时的密码校验。
安装 argon2
pnpm install argon2
bash
argon2 官方文档建议:不要自定义 salt 参数,除非你完全理解其工作原理。默认 salt 长度为 16 字节,已经足够安全。
注册:密码哈希
在用户创建前对密码进行哈希处理:
// user/repository/user.repository.ts
import * as argon2 from 'argon2';
@Injectable()
export class UserRepository {
async create(user: CreateUserDto) {
const { password } = user;
if (password) {
// 使用 argon2 哈希密码(自动生成盐值)
const hashedPassword = await argon2.hash(password);
user.password = hashedPassword;
}
return this.prisma.user.create({ data: user });
}
}
typescript
验证效果:即使两次注册使用相同密码,生成的哈希值也完全不同:
# 注册用户 A(密码: 123456)
# 响应: { password: "$argon2id$v=19$m=65536,t=3,p=4$aB3dE7f..." }
# 注册用户 B(密码: 123456)
# 响应: { password: "$argon2id$v=19$m=65536,t=3,p=4$X9yK2mP..." }
# ↑ 完全不同,因为每次自动生成不同的盐值
bash
注册:用户名唯一性检查
在创建用户前先查询是否已存在同名用户:
// auth/auth.service.ts
import * as argon2 from 'argon2';
@Injectable()
export class AuthService {
constructor(private userRepository: UserRepository) {}
async signUp(dto: SignUpDto) {
// 先查询用户是否已存在(读操作,性能优于写)
const user = await this.userRepository.findByUsername(dto.username);
if (user.length > 0) {
throw new ForbiddenException('用户已存在');
}
// 创建用户(密码在 Repository 中自动哈希)
const newUser = await this.userRepository.create(dto);
return newUser;
}
}
typescript
登录:密码校验
使用 argon2.verify() 对比明文密码与存储的哈希值:
// auth/auth.service.ts
async signIn(dto: SignInDto) {
// 查询用户
const user = await this.userRepository.findByUsername(dto.username);
if (user.length === 0) {
throw new ForbiddenException('用户不存在');
}
// 使用 argon2 校验密码
const isPasswordValid = await argon2.verify(
user[0].password, // 数据库中的哈希值
dto.password, // 用户输入的明文密码
);
if (!isPasswordValid) {
throw new ForbiddenException('密码错误');
}
// 生成 JWT Token
const token = this.jwtService.sign({
username: user[0].username,
sub: user[0].id,
});
return { access_token: token };
}
typescript
argon2 核心 API
import * as argon2 from 'argon2';
// 哈希密码
const hash = await argon2.hash('password123');
// → "$argon2id$v=19$m=65536,t=3,p=4$salt$hash..."
// 校验密码(返回 boolean)
const isValid = await argon2.verify(hash, 'password123'); // true
const isInvalid = await argon2.verify(hash, 'wrongPass'); // false
// 检查是否需要重新哈希(参数升级时使用)
const needsRehash = argon2.needsRehash(hash);
typescript
| API | 说明 | 返回值 |
|---|---|---|
argon2.hash(password, options?) | 哈希密码,自动生成盐值 | string(编码后的哈希字符串) |
argon2.verify(hash, password, options?) | 校验密码是否匹配哈希 | boolean |
argon2.needsRehash(hash, options?) | 检查哈希是否需要升级参数 | boolean |
脏数据处理策略
当数据库中存在明文密码的旧数据时,argon2 校验会失败并抛出异常。处理策略:
| 阶段 | 策略 |
|---|---|
| 开发期 | 直接删除数据库中的脏数据 |
| 生产期 | 编写 SQL 迁移脚本批量重新哈希,操作前必须备份数据库 |
不要用 try-catch 绕过校验失败,而是从根本上解决数据一致性问题。
完整注册登录流程图
注册流程:
前端 → POST /auth/signup { username, password }
→ ValidationPipe 校验
→ AuthService.signUp()
→ 查询用户是否已存在(读)
→ UserRepository.create()
→ argon2.hash(password) 自动加盐哈希
→ Prisma 写入数据库
→ 响应(需脱敏)
登录流程:
前端 → POST /auth/signin { username, password }
→ ValidationPipe 校验
→ AuthService.signIn()
→ 查询用户(读)
→ argon2.verify(dbHash, inputPassword)
→ jwtService.sign() 生成 Token
→ 响应 { access_token: "..." }
text
↑